using System;
using System.Data.Linq;
using System.Data.Linq.Mapping;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace OpenQuarters.EntityBase
{
    /// <summary>
    /// Abstract Linq repository base class written by Adrian Grigore
    /// 
    /// RepositoryBase supports disconnected mode and recursive saving / deleting of linq entities
    /// 
    /// For details about this class, updated versions, and more useful tips please visit
    /// http://devermind.com/linq/a-linq-disconnected-mode-abstract-base-class
    ///     
    ///     
    /// Released under The Code Project Open License (CPOL)
    /// </summary>
    /// <typeparam name="TEntityType"></typeparam>
    /// <typeparam name="TContextType"></typeparam>
    [Serializable]
    public abstract class RepositoryBase<TEntityType, TKeyType, TContextType>
        where TEntityType : EntityBase<TEntityType, TKeyType, TContextType>
        where TContextType : DataContext
    {
        public class EntityRepository : RepositoryBase<TEntityType, TKeyType, TContextType>
        {
            private TEntityType _sampleInstance = null;

            public override System.Linq.Expressions.Expression<Func<TEntityType, bool>> GetIDSelector(TKeyType ID)
            {
                if (_sampleInstance == null)
                {
                    _sampleInstance = Activator.CreateInstance<TEntityType>();
                }
                return _sampleInstance.GetIDSelector(ID);
            }
        }

        protected RepositoryBase()
        {
            //make sure TEntityType and all associated entities are suitable for use with this class. 
            Debug.Assert(CheckEntityConstraints());
        }

        /// <summary>
        ///selection delegate expression, used for speeding up Load(int ID)
        /// needs to be implemented by Repository descendants as follows:
        ///
        /// protected override Expression<Func<SpecializedTEntityType, bool>> GetIDSelector(int ID)
        ///{
        ///    return (item) => item.ID == ID;
        ///}
        /// 
        /// SpecializedTEntityType represents the given entity's type
        /// and item.ID represents the entity's unique ID property
        /// 
        /// </summary>
        public abstract Expression<Func<TEntityType, bool>> GetIDSelector(TKeyType ID);


        /// <summary>
        /// creates a new datacontext and loads the entity with the given unique ID
        /// </summary>
        /// <param name="ID">the unique entity ID</param>
        /// <returns>the entity if the ID exists, null otherwise</returns>
        public TEntityType Load(TKeyType ID)
        {
            TContextType Context = EntityBase<TEntityType, TKeyType, TContextType>.DataContext;
            return Load(ID, Context);
        }

        /// <summary>
        /// loads the entity with the given unique ID from the given context
        /// </summary>
        /// <param name="ID">the unique entity ID</param>
        /// <param name="context">the datacontext</param>
        /// <returns>the entity if the ID exists, null otherwise</returns>
        protected TEntityType Load(TKeyType ID, TContextType context)
        {
            return GetEntityTable(context).FirstOrDefault(GetIDSelector(ID));
        }

        /// <summary>
        /// Update or insert the Linq entity to the database,
        /// ignoring child entities
        /// </summary>
        /// <returns>true on success, false otherwise</returns>
        public virtual TEntityType Save(TEntityType ToSave)
        {
            return ExecuteDatabaseOperation(ToSave, OpMode.Save, false);
        }

        /// <summary>
        /// Updates or inserts the Linq entity into the database,
        /// Child entities are saved recursively
        /// </summary>
        /// <returns>true on success, false otherwise</returns>
        public virtual TEntityType SaveRecursively(TEntityType ToSave)
        {
            return ExecuteDatabaseOperation(ToSave, OpMode.Save, true);
        }

        /// <summary>
        ///  Deletes the given Linq entity, ignoring child entities
        /// </summary>
        /// <returns>True on success, false otherwise</returns>
        public virtual TEntityType Delete(TEntityType ToDelete)
        {
            return ExecuteDatabaseOperation(ToDelete, OpMode.Delete, false);
        }

        /// <summary>
        ///  Deletes the given Linq entity 
        ///  All child entities are also deleted
        /// </summary>
        /// <returns>True on success, false otherwise</returns>
        public virtual TEntityType DeleteRecursively(TEntityType ToDelete)
        {
            return ExecuteDatabaseOperation(ToDelete, OpMode.Delete, true);
        }

        /// <summary>
        /// Deletes the Linq entity with the given ID
        /// Child Entities are not deleted
        /// </summary>        
        /// <returns>true on success, false otherwise</returns>
        public TEntityType Delete(TKeyType ID)
        {
            return Delete(Load(ID));
        }


        /// <summary>
        /// Deletes the Linq entity with the given ID
        /// All preloaded child entities are also deleted
        /// </summary>        
        /// <returns>true on success, false otherwise</returns>
        public TEntityType DeleteRecursively(TKeyType ID)
        {
            return DeleteRecursively(Load(ID));
        }

        #region Internal and helper methods

        /// <summary>
        /// Save / Insert / Delete the given Linq entity depending on the given OperationMode  
        /// </summary>
        /// <returns>true on success, false otherwise</returns>
        private TEntityType ExecuteDatabaseOperation(TEntityType theEntity, OpMode OperationMode, bool Recursively)
        {
            using (TContextType context = EntityBase<TEntityType, TKeyType, TContextType>.DataContext)
            {
                //make sure the entity is not attached to an old datacontext 
                theEntity = EntityDetacher<TEntityType>.Detach(theEntity);
                Table<TEntityType> entityTable = context.GetTable<TEntityType>();

                //for some unknown reason attaching the entity before deleting it is only necessary
                //on the topmost entity, but not on the child entities
                if (OperationMode == OpMode.Delete)
                {
                    entityTable.Attach(theEntity, true);
                }

                IterateEntity(theEntity, context, entityTable, OperationMode, Recursively);
                context.SubmitChanges();

                return theEntity;
            }
        }


        /// <summary>
        /// Save / Insert / Delete the contents of the given EntitySet
        /// </summary>
        internal void IterateEntitySet(object Set, TContextType context, OpMode OperationMode, bool Recursively)
        {
            Table<TEntityType> entityTable = GetEntityTable(context);
            foreach (TEntityType NextEntity in (EntitySet<TEntityType>)Set)
            {
                IterateEntity(NextEntity, context, entityTable, OperationMode, Recursively);
            }
        }


        /// <summary>
        /// Save / Insert / Delete the given Linq entity depending on the given OperationMode  
        /// </summary>
        /// <returns>true on success, false otherwise</returns>
        private void IterateEntity(TEntityType theEntity, TContextType context, Table<TEntityType> EntityTable, OpMode OperationMode, bool Recursively)
        {
            Debug.Assert(HasValidVersionProperty(theEntity, context));

            if (Recursively)
            {
                foreach (MetaAssociation association in context.Mapping.GetMetaType(typeof(TEntityType)).Associations)
                {
                    //only 1:n child entitites are which have been loaded are saved 
                    //check for 1:n relationship
                    if (association.IsMany && association.ThisKeyIsPrimaryKey)
                    {
                        PropertyInfo AssociationProperty = theEntity.GetType().GetProperty(association.ThisMember.Name);
                        //make sure there is at least one child entity to save
                        if (AssociationProperty.PropertyType.Name == "EntitySet`1")
                        {
                            //save the associated child entities
                            try
                            {
                                Type entityBaseType = association.OtherType.Type;
                                while (!entityBaseType.FullName.Contains(".EntityBase`"))
                                {
                                    entityBaseType = entityBaseType.BaseType;
                                }

                                Type[] genericTypes = entityBaseType.GetGenericArguments();
                                Type entityType = genericTypes[0];
                                Type keyType = genericTypes[1];
                                Type contextType = genericTypes[2];

                                object Repository = Activator.CreateInstance(Type.GetType("OpenQuarters.EntityBase.EntityRepository`3").MakeGenericType(entityType, keyType, contextType));
                                Repository.GetType().GetMethod("IterateEntitySet",
                                                             BindingFlags.NonPublic | BindingFlags.Instance).Invoke(
                                    Repository,
                                    new object[4]
                                    {
                                        AssociationProperty.GetValue(theEntity, null),
                                        context,
                                        OperationMode,
                                        Recursively
                                    }
                                    );
                            }
                            catch (System.Reflection.TargetInvocationException e)
                            {
                                throw (e.InnerException);
                            }
                        }
                    }
                }
            }

            switch (OperationMode)
            {
                case OpMode.Save:
                    if (theEntity.IsNew())
                    {
                        EntityTable.InsertOnSubmit(theEntity);
                    }
                    else
                    {

                        EntityTable.Attach(theEntity, true);
                    }
                    break;
                case OpMode.Delete:
                    try
                    {
                        EntityTable.DeleteOnSubmit(theEntity);
                    }
                    catch (Exception test)
                    {
                        Debug.Write("e:" + test.Message);
                    }
                    break;
            }


        }


        /// <summary>
        /// there are two constraints on the entity types that this Repositorybase is used with:
        /// - All entities must have a unique ID property
        /// - Each entity must  must implement the CreateRepository method which returns a concrete RepositoryBase descendant that matches the entity type
        /// this method checks all three constraints
        /// the checks are only carried out in when the application runs in debug mode
        /// </summary>
        private bool CheckEntityConstraints()
        {
            var contextKey = "OpenQuarters.EntityBase.RepositoryBase<" + typeof(TEntityType).FullName + ", " + typeof(TKeyType).FullName + ", " + typeof(TContextType).FullName + ">.CheckEntityConstraints";
            if (!OpenQuarters.EntityBase.Utils.ContextItem.GetItem(contextKey, () => false))
            {
                TContextType context = EntityBase<TEntityType, TKeyType, TContextType>.DataContext;

                //a version attribute is not required, but advisable for speeding up 
                //save and delete operations
                if (!HasVersionProperty(context))
                {
                    Debug.WriteLine("Warning: \"" +
                                                      typeof(TEntityType).Name +
                                                      "\" entity type does not have a version property. You might want to add a version column to speed up Saving and Deleting of \"" +
                                                      typeof(TEntityType).Name +
                                                      "\" entities");
                }

                //make sure that TEntityType has a version attribute
                if (context.Mapping.GetTable(typeof(TEntityType)).RowType.IdentityMembers.Count != 1)
                {
                    throw new NotImplementedException("\"" +
                                                      typeof(TEntityType).Name +
                                                      "\" entity type does not have a unique ID property");
                }

                /// make sure that all child linq entities of TEntityType implement a suitable "CreateRepository" Method 
                //foreach (MetaAssociation association in context.Mapping.GetMetaType(typeof(TEntityType)).Associations)
                //{
                //    if (association.OtherType.Type.GetMethod("CreateRepository") == null)
                //    {
                //        throw new NotImplementedException("\"" + association.OtherType.Type.Name +
                //                                          "\" entity type does not implement a RepositoryBase<SpecializedEntityType,DataClassesDataContext> CreateRepository() method.");
                //    }
                //}
                return true;
            }
            return true;
        }

        private static bool HasVersionProperty(TContextType context)
        {
            return context.Mapping.GetTable(typeof(TEntityType)).RowType.VersionMember != null;
        }

        /// <summary>
        /// only new Linq entities are allowed to have a null version property. 
        /// if this is not the case then the version propery has probably gone lost while displaying the entity in a databound control
        /// </summary>
        /// <param name="Entity"></param>
        /// <returns></returns>
        private bool HasValidVersionProperty(TEntityType Entity, TContextType context)
        {
            
            if (
                HasVersionProperty(context)
                )if (
                !Entity.IsNew()
                && Entity.GetType().GetProperty(GetEntityVersionFieldName(context)).GetValue(Entity, null) == null)
            {
                throw new ApplicationException(Entity.GetType().Name +
                                               " has a non-zero identity property, but a null version. Check your databound controls to make sure the version attribute is retained.");
            }

            return true;
        }

        private Table<TEntityType> GetEntityTable(TContextType context)
        {
            return (Table<TEntityType>)context.GetTable(typeof(TEntityType));
        }

        private String GetEntityVersionFieldName(TContextType context)
        {
            return context.Mapping.GetTable(typeof(TEntityType)).RowType.VersionMember.Name;
        }

        /// <summary>
        /// Saving and Deleting entities works in a very similar fashion, so I am using using a 
        /// template function with different opmode parameters for both operations
        /// </summary>
        internal enum OpMode
        {
            Delete,
            Save
        } ;

        #endregion
    }
}